using System;
using System.Globalization;
using System.IO;
using System.Web;
using System.Linq.Expressions;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Web.Mvc;
using System.Web.Routing;
using System.Text.RegularExpressions;
using System.Text;
using System.Security.Cryptography;
using System.Web.UI;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Web.Configuration;
using NuGet.Enterprise.Server.Xml;
using System.Web.Caching;
using NuGet.Enterprise.Server.Xml.Xsl;

namespace NuGet.Enterprise.Server
{
    public static class Extensions
    {
        private static readonly object CacheSync = new object();

        private const string StringFormatFallbackPattern = @"\{[\d]+[\:]?[\s\w\.\,\%\+\(\)#]*\}";

        private const string PackageFeedStylesheet = "~/content/transform/PkgTransform.xslt";

        #region Extend: System.Web.Mvc.HtmlHelper<dynamic>

        public static MvcHtmlString RenderNavigationLink(this HtmlHelper<dynamic> html, string text, string url)
        {
            var wildcard = url.EndsWith("/*");

            if (wildcard)
            {
                url = url.Substring(0, url.Length - 1);
            }

            var link = new TagBuilder("a");

            link.SetInnerText(text);
            link.Attributes.Add("href", url);

            var item = new TagBuilder("li")
            {
                InnerHtml = link.ToString()
            };

            var path = HttpContext.Current.Request.Url.AbsolutePath;

            if (!path.EndsWith("/"))
            {
                path += "/";
            }

            if (wildcard ? path.StartsWith(url, StringComparison.InvariantCultureIgnoreCase) : string.Equals(path, url, StringComparison.OrdinalIgnoreCase))
            {
                item.AddCssClass("current-tab");
            }

            return new MvcHtmlString(item.ToString());
        }

        public static MvcHtmlString RenderXmlTransform(this HtmlHelper<dynamic> html, string xmlUri, string xslUri, XsltArgumentList arguments = null)
        {
            if (string.IsNullOrEmpty(xmlUri))
            {
                throw new ArgumentNullException("xmlUri");
            }

            using (var xml = XmlReader.Create(xmlUri.ResolveUri(html.ViewContext.HttpContext)))
            using (var xsl = (string.IsNullOrEmpty(xslUri) ? null : XmlReader.Create(xslUri.ResolveUri(html.ViewContext.HttpContext))))
            {
                return html.RenderXmlTransform(xml, xsl, arguments);
            }
        }

        public static MvcHtmlString RenderXmlTransform(this HtmlHelper<dynamic> html, XmlReader xml, XmlReader xsl, XsltArgumentList arguments = null)
        {
            if (xml == null)
            {
                throw new ArgumentNullException("xml");
            }

            var xslDoc = new XslCompiledTransform();

            if (xsl == null)
            {
                var url = html.ViewContext.HttpContext.Server.MapPath("~/content/transform/copytransform.xslt");

                xslDoc.Load(url);
            }
            else
            {
                xslDoc.Load(xsl);
            }

            arguments = arguments ?? new XsltArgumentList();
            arguments.AddExtensionObject("urn:Extensions", new XsltExtensions());

            using (var output = new StringWriter())
            using (var source = new XmlContentReader(xml))
            {
                xslDoc.Transform(source, arguments, output);

                var result = output.GetStringBuilder().ToString();

                Debug.WriteLine("XSLT Result: " + result);

                return new MvcHtmlString(result);
            }
        }

        public static MvcHtmlString RenderPackageFeed(this HtmlHelper<dynamic> html, string serviceUri, string filter, int pageIndex, int pageSize, string detailUri)
        {
            var xmlUri = serviceUri.ResolveUri(html.ViewContext.HttpContext);
            var xslUri = PackageFeedStylesheet.ResolveUri(html.ViewContext.HttpContext);

            using (var xml = new XmlPackageReader(xmlUri, filter, pageIndex, pageSize))
            using (var xsl = System.Xml.XmlReader.Create(xslUri))
            {
                var arguments = new XsltArgumentList();

                if (!string.IsNullOrEmpty(detailUri))
                {
                    arguments.AddParam("detailUri", string.Empty, detailUri);
                }

                return html.RenderXmlTransform(xml, xsl, arguments);
            }
        }

        #endregion

        #region Extend: System.String

        public static string ResolveUri(this string instance, HttpContextBase context)
        {
            if (context == null)
            {
                throw new ArgumentException("context");
            }

            if (!string.IsNullOrEmpty(instance) && instance.StartsWith("~/"))
            {
                var requestUrl = context.Request.Url;

                if (requestUrl != null)
                {
                    var host = requestUrl.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);

                    return host + instance.Substring(1);
                }
            }

            return instance;
        }

        /// <summary>
        /// Formats the current instance.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="provider">The format provider to use.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string Format(this string instance, IFormatProvider provider, object arg0)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            try
            {
                return string.Format(provider, instance, arg0);
            }
            catch (FormatException)
            {
                return Regex.Replace(instance, StringFormatFallbackPattern, delegate(Match match)
                {
                    return match.Value.Format(provider, arg0);
                });
            }
        }

        /// <summary>
        /// Formats the current instance.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="provider">The format provider to use.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <param name="arg1">The second argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string Format(this string instance, IFormatProvider provider, object arg0, object arg1)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            try
            {
                return string.Format(provider, instance, arg0, arg1);
            }
            catch (FormatException)
            {
                return Regex.Replace(instance, StringFormatFallbackPattern, delegate(Match match)
                {
                    return match.Value.Format(provider, arg0, arg1);
                });
            }
        }

        /// <summary>
        /// Formats the current instance.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="provider">The format provider to use.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <param name="arg1">The second argument to merge.</param>
        /// <param name="arg2">The third argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string Format(this string instance, IFormatProvider provider, object arg0, object arg1, object arg2)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            try
            {
                return string.Format(provider, instance, arg0, arg1, arg2);
            }
            catch (FormatException)
            {
                return Regex.Replace(instance, StringFormatFallbackPattern, delegate(Match match)
                {
                    return match.Value.Format(provider, arg0, arg1, arg2);
                });
            }
        }

        /// <summary>
        /// Formats the current instance.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="provider">The format provider to use.</param>
        /// <param name="args">A set of arguments to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string Format(this string instance, IFormatProvider provider, params object[] args)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            try
            {
                return string.Format(provider, instance, args);
            }
            catch (FormatException)
            {
                return Regex.Replace(instance, StringFormatFallbackPattern, delegate(Match match)
                {
                    return match.Value.Format(provider, args);
                });
            }
        }

        /// <summary>
        /// Formats the current instance based on the invariant culture's format provider.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string FormatInvariant(this string instance, object arg0)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            return instance.Format(CultureInfo.InvariantCulture, arg0);
        }

        /// <summary>
        /// Formats the current instance based on the invariant culture's format provider.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <param name="arg1">The second argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string FormatInvariant(this string instance, object arg0, object arg1)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            return instance.Format(CultureInfo.InvariantCulture, arg0, arg1);
        }

        /// <summary>
        /// Formats the current instance based on the invariant culture's format provider.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <param name="arg1">The second argument to merge.</param>
        /// <param name="arg2">The third argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string FormatInvariant(this string instance, object arg0, object arg1, object arg2)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            return instance.Format(CultureInfo.InvariantCulture, arg0, arg1, arg2);
        }

        /// <summary>
        /// Formats the current instance based on the invariant culture's format provider.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="args">A set of arguments to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string FormatInvariant(this string instance, params object[] args)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            return instance.Format(CultureInfo.InvariantCulture, args);
        }

        /// <summary>
        /// Computes the hash.
        /// </summary>
        /// <typeparam name="P"></typeparam>
        /// <param name="instance">The instance.</param>
        /// <returns></returns>
        public static string ComputeHash<P>(this string instance)
            where P : HashAlgorithm, new()
        {
            if (instance == null)
            {
                return null;
            }

            using (P provider = new P())
            {
                byte[] crypt = provider.ComputeHash(Encoding.Unicode.GetBytes(instance));

                StringBuilder output = new StringBuilder();

                foreach (byte b in crypt)
                {
                    string hex = b.ToString("x2", CultureInfo.InvariantCulture);

                    output.Append(hex.ToUpperInvariant());
                }

                return output.ToString();
            }
        }

        #endregion

        #region Extend: System.Diagnostic.Stopwatch

        public static IDisposable Monitor(this Stopwatch instance, Action<TimeSpan> callback)
        {
            if (!instance.IsRunning)
            {
                instance.Start();
            }

            return new Disposable
            (
                () =>
                {
                    if (instance.IsRunning)
                    {
                        instance.Stop();
                    }

                    if (callback != null)
                    {
                        callback(instance.Elapsed);
                    }
                }
            );
        }

        #endregion

        #region Extend: System.Web.HttpRequest

        internal static bool TryGetQueryStringValue<T>(this HttpRequest instance, string key, out T value)
            where T : class
        {
            value = default(T);

            var val = (from k in instance.QueryString.AllKeys
                       where k.Equals(key, StringComparison.OrdinalIgnoreCase)
                       select (instance.QueryString[k] ?? string.Empty)).FirstOrDefault();

            if (val == null) return false;

            try
            {
                if (typeof(T) == typeof(bool))
                {
                    bool flag;

                    if (val.Trim().Length == 0)
                    {
                        flag = true;
                    }
                    else if (!bool.TryParse(val, out flag))
                    {
                        flag = XmlConvert.ToBoolean(val);
                    }

                    value = (T)((object)flag);
                }
                else
                {
                    value = (T)Convert.ChangeType(val, typeof(T));
                }

                return true;
            }
            catch (Exception exc)
            {
                throw new InvalidCastException("Unable to convert '{0}' to type '{1}'.".FormatInvariant(val, typeof(T).FullName), exc);
            }
        }

        #endregion

        #region Extend: System.Web.Caching.Cache

        internal static object Get(this Cache instance, string key, Action factory)
        {
            var value = instance.Get(key);

            if (value == null && factory != null)
            {
                lock (CacheSync)
                {
                    value = instance.Get(key);

                    if (value == null)
                    {
                        factory();

                        value = instance.Get(key);
                    }
                }
            }

            return value;
        }

        #endregion

        #region Extend: System.Web.Routing.RouteValueDictionary

        public static void AddGroupValues(this RouteValueDictionary values, GroupCollection groups, string[] names)
        {
            if (null != names && null != groups && null != values)
            {
                foreach (var name in from name in names where !char.IsNumber(name, 0) select name)
                {
                    try { values[name] = groups[name].Value; }
                    catch { }
                }
            }
        }

        #endregion

        #region Extend: System.DateTimeOffset

        public static DateTimeOffset TrimMilliseconds(this DateTimeOffset value)
        {
            return new DateTimeOffset(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, TimeSpan.Zero);
        }

        #endregion

        #region Extend: System.Web.HttpRequestBase

        public static bool IsUnmodified(this HttpRequestBase request, DateTimeOffset resourceLastModified)
        {
            DateTimeOffset ifModifiedSince;

            // If-Modified-Since in format "Fri, 02 Oct 2010 15:50:12 GMT" (RFC1123)
            if (DateTimeOffset.TryParseExact(request.Headers["If-Modified-Since"], "R", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out ifModifiedSince))
            {
                if (resourceLastModified.ToUniversalTime().TrimMilliseconds() <= ifModifiedSince.ToUniversalTime())
                {
                    return true;
                }
            }

            return false;
        }

        public static bool TryGetQueryStringValue<T>(this HttpRequestBase request, string name, out T value)
        {
            var names = new List<string>(request.QueryString.AllKeys);

            var val = (from n in names
                       where string.Equals(n, name, StringComparison.OrdinalIgnoreCase)
                       select request.QueryString[n]).FirstOrDefault();

            if (val != null)
            {
                value = (T)Convert.ChangeType(val, typeof(T));

                return true;
            }

            value = default(T);

            return false;
        }

        #endregion

        #region Extend: System.Collections.Generic.IEnumerable<T>

        private static readonly bool RewritingRequired = IsRewritingRequired();

        /// <summary>
        /// Replacing closures with constant values is required only when executing in partial trust and the NuGet assembly is GACed.
        /// </summary>
        private static bool IsRewritingRequired()
        {
            AppDomain appDomain = AppDomain.CurrentDomain;
            Assembly assembly = MethodBase.GetCurrentMethod().DeclaringType.Assembly;
            return appDomain.IsHomogenous && !appDomain.IsFullyTrusted && assembly.IsFullyTrusted;
        }

        /// <summary>
        /// The purpose of this method is to mitigate a partial trust issue. We expose
        /// EnumerableQuery (wrapping an enumrable in an IQueryable) throughout the codebase
        /// and expression compilation doesn't work in some cases. See SafeEnumerableQuery for more details.
        /// </summary>
        internal static IQueryable<T> AsSafeQueryable<T>(this IEnumerable<T> source)
        {
            return AsSafeQueryable(source, rewriteQuery: RewritingRequired);
        }

        internal static IQueryable<T> AsSafeQueryable<T>(this IEnumerable<T> source, bool rewriteQuery)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (rewriteQuery)
            {
                return new SafeEnumerableQuery<T>(source);
            }
            // AsQueryable returns the original source if it is already a IQueryable<T>. 
            return source.AsQueryable();
        }

        #endregion
    }

    internal static class QueryableUtility
    {
        private static readonly string[] _orderMethods = new[] { "OrderBy", "ThenBy", "OrderByDescending", "ThenByDescending" };
        private static readonly MethodInfo[] _methods = typeof(Queryable).GetMethods(BindingFlags.Public | BindingFlags.Static);

        private static MethodInfo GetQueryableMethod(Expression expression)
        {
            if (expression.NodeType == ExpressionType.Call)
            {
                var call = (MethodCallExpression)expression;

                if (call.Method.IsStatic && call.Method.DeclaringType == typeof(Queryable))
                {
                    return call.Method.GetGenericMethodDefinition();
                }
            }

            return null;
        }

        public static bool IsQueryableMethod(Expression expression, string method)
        {
            return _methods.Where(m => m.Name == method).Contains(GetQueryableMethod(expression));
        }

        public static bool IsOrderingMethod(Expression expression)
        {
            return _orderMethods.Any(method => IsQueryableMethod(expression, method));
        }

        public static Expression ReplaceQueryableExpression(IQueryable query, Expression expression)
        {
            return new ExpressionRewriter(query).Visit(expression);
        }

        public static Type FindGenericType(Type definition, Type type)
        {
            while ((type != null) && (type != typeof(object)))
            {
                if (type.IsGenericType && (type.GetGenericTypeDefinition() == definition))
                {
                    return type;
                }
                if (definition.IsInterface)
                {
                    foreach (Type interfaceType in type.GetInterfaces())
                    {
                        Type genericType = FindGenericType(definition, interfaceType);
                        if (genericType != null)
                        {
                            return genericType;
                        }
                    }
                }
                type = type.BaseType;
            }
            return null;
        }

        private class ExpressionRewriter : ExpressionVisitor
        {
            private readonly IQueryable _query;

            public ExpressionRewriter(IQueryable query)
            {
                _query = query;
            }

            protected override Expression VisitConstant(ConstantExpression node)
            {
                // Replace the query at the root of the expression
                if (typeof(IQueryable).IsAssignableFrom(node.Type))
                {
                    return _query.Expression;
                }
                return base.VisitConstant(node);
            }
        }
    }

    /// <summary>
    /// There are some seurity issues around evaluating queries over EnumerableQuery with clousures.
    /// The compiler generates an internal type that can't be causes expression compilation to fail, when the 
    /// calling assembly is in the GAC and is SecurityTransparent. We wrap the underlying enumerable query and 
    /// then remove all compiler generated closures from the expression before compilation.
    /// </summary>
    [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Type is an IQueryable and by convention should end with the term Query")]
    internal class SafeEnumerableQuery<T> : IQueryable<T>, IQueryProvider, IOrderedQueryable<T>
    {
        private readonly IQueryable _enumerableQuery;
        private readonly Expression _expression;

        public SafeEnumerableQuery(IEnumerable<T> enumerable)
        {
            if (enumerable == null)
            {
                throw new ArgumentNullException("enumerable");
            }
            _enumerableQuery = enumerable.AsQueryable();
            _expression = Expression.Constant(this);
        }

        private SafeEnumerableQuery(IQueryable enumerableQuery, Expression expression)
        {
            _enumerableQuery = enumerableQuery;
            _expression = expression;
        }

        public IEnumerator<T> GetEnumerator()
        {
            // Create the new query and return the enumerator
            return InnerProvider.CreateQuery<T>(InnerExpression).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public Type ElementType
        {
            get { return typeof(T); }
        }

        public Expression Expression
        {
            get { return _expression; }
        }

        public IQueryProvider Provider
        {
            get { return this; }
        }

        private IQueryProvider InnerProvider
        {
            get { return _enumerableQuery.Provider; }
        }

        internal Expression InnerExpression
        {
            get { return GetInnerExpression(Expression); }
        }

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            return (IQueryable<TElement>)CreateQuery(typeof(TElement), expression);
        }

        public IQueryable CreateQuery(Expression expression)
        {
            // Copied logic from EnumerableQuery
            if (expression == null)
            {
                throw new ArgumentNullException("expression");
            }

            Type elementType = QueryableUtility.FindGenericType(typeof(IQueryable<>), expression.Type);

            if (elementType == null)
            {
                throw new ArgumentException(String.Empty, "expression");
            }

            return CreateQuery(elementType, expression);
        }

        public TResult Execute<TResult>(Expression expression)
        {
            return InnerProvider.Execute<TResult>(GetInnerExpression(expression));
        }

        public object Execute(Expression expression)
        {
            return InnerProvider.Execute(GetInnerExpression(expression));
        }

        private Expression GetInnerExpression(Expression expression)
        {
            // First replace the this IQueryable with the enumerable query
            expression = QueryableUtility.ReplaceQueryableExpression(_enumerableQuery, expression);

            // Evaluate the closure variables
            return new ClosureEvaluator().Visit(expression);
        }

        private IQueryable CreateQuery(Type elementType, Expression expression)
        {
            var queryType = typeof(SafeEnumerableQuery<>).MakeGenericType(elementType);
            var ctor = queryType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).Single();

            return (IQueryable)ctor.Invoke(new object[] { _enumerableQuery, expression });
        }

        public override string ToString()
        {
            return _enumerableQuery.ToString();
        }
    }

    internal class ClosureEvaluator : ExpressionVisitor
    {
        // For unit testing. We want to circumvent the assembly check during unit testing since
        // closures will be generated in that assembly.
        private bool _checkAssemly;

        internal ClosureEvaluator(bool checkAssembly = true)
        {
            _checkAssemly = checkAssembly;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            if (IsGeneratedClosureMember(node))
            {
                var constantExpression = (ConstantExpression)node.Expression;
                var fieldInfo = (FieldInfo)node.Member;
                // Evaluate the closure member
                return Expression.Constant(GetValue(node, fieldInfo, constantExpression.Value));
            }
            return base.VisitMember(node);
        }

        private object GetValue(MemberExpression node, FieldInfo fieldInfo, object obj)
        {
            if (_checkAssemly)
            {
                Type parentType = node.Expression.Type.DeclaringType;
                Debug.Assert(parentType != null, "Not in a compiler generated closure type");

                // Since the closure class is private sealed, we're going to look for an eval method on that class
                // where it's ok to look up field info.
                MethodInfo evalMethodInfo = parentType.GetMethod("Eval", BindingFlags.NonPublic | BindingFlags.Static);
                Debug.Assert(evalMethodInfo != null, "Eval method cannot be found. Please add and Eval(FieldInfo info, object value) to " + parentType.FullName);

                // Invoke that method
                return evalMethodInfo.Invoke(null, new object[] { fieldInfo, obj });
            }

            // This only happens in the unit test
            return fieldInfo.GetValue(obj);
        }

        protected override Expression VisitConstant(ConstantExpression node)
        {
            return base.VisitConstant(node);
        }

        private bool IsGeneratedClosureMember(MemberExpression node)
        {
            // Closure types are internal classes that are compiler generated in our own assembly
            return node.Expression != null &&
                   node.Member != null &&
                   node.Expression.NodeType == ExpressionType.Constant &&
                   node.Member.MemberType == MemberTypes.Field &&
                   !node.Expression.Type.IsVisible &&
                   CheckAssembly(node.Member) &&
                   IsCompilerGenerated(node.Expression.Type);
        }

        private bool CheckAssembly(MemberInfo member)
        {
            if (_checkAssemly)
            {
                // Make sure we're in our assembly
                return member.DeclaringType.Assembly == typeof(ClosureEvaluator).Assembly;
            }

            // This is only the case for unit tests
            return true;
        }

        private static bool IsCompilerGenerated(Type type)
        {
            return type.GetCustomAttributes(inherit: true)
                       .OfType<CompilerGeneratedAttribute>()
                       .Any();
        }
    }
}